{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Boustrophedon Formulation and Tests"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-31T17:35:10.663236Z",
     "start_time": "2020-07-31T17:35:10.370677Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/alan/opt/anaconda3/envs/data_sci/lib/python3.7/importlib/_bootstrap.py:219: RuntimeWarning: numpy.ufunc size changed, may indicate binary incompatibility. Expected 192 from C header, got 216 from PyObject\n",
      "  return f(*args, **kwds)\n",
      "/Users/alan/opt/anaconda3/envs/data_sci/lib/python3.7/importlib/_bootstrap.py:219: RuntimeWarning: numpy.ufunc size changed, may indicate binary incompatibility. Expected 192 from C header, got 216 from PyObject\n",
      "  return f(*args, **kwds)\n",
      "/Users/alan/opt/anaconda3/envs/data_sci/lib/python3.7/importlib/_bootstrap.py:219: RuntimeWarning: numpy.ufunc size changed, may indicate binary incompatibility. Expected 192 from C header, got 216 from PyObject\n",
      "  return f(*args, **kwds)\n"
     ]
    }
   ],
   "source": [
    "import math\n",
    "import time\n",
    "import numpy as np\n",
    "from tqdm.auto import tqdm\n",
    "from warnings import warn"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-31T17:35:08.075180Z",
     "start_time": "2020-07-31T17:35:08.069735Z"
    }
   },
   "outputs": [],
   "source": [
    "from cpp_algorithms.common_helpers import is_valid, adjacency_test\n",
    "from cpp_algorithms.common_helpers import imshow, imshow_scatter\n",
    "from cpp_algorithms.common_helpers import get_all_area_maps, get_random_coords, get_area_map\n",
    "from cpp_algorithms.testers.metrics import coverage_metrics\n",
    "from cpp_algorithms.testers.display_funcs import printer"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A - Star\n",
    "(untouched)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:25.600626Z",
     "start_time": "2020-07-28T19:57:25.592921Z"
    }
   },
   "outputs": [],
   "source": [
    "def heuristic(start, goal):\n",
    "    #Use Chebyshev distance heuristic if we can move one square either\n",
    "    #adjacent or diagonal\n",
    "    D = 1\n",
    "    D2 = 1\n",
    "    dx = abs(start[0] - goal[0])\n",
    "    dy = abs(start[1] - goal[1])\n",
    "    return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy)\n",
    "    \n",
    "def get_vertex_neighbours(pos, diameter, width, height):\n",
    "    n = []\n",
    "    #Moves allow link a chess king\n",
    "    for dx, dy in [(diameter,0),(-diameter,0),(0,diameter),(0,-diameter)]:\n",
    "        x2 = pos[0] + dx\n",
    "        y2 = pos[1] + dy\n",
    "        if x2 < 0 or x2 > width-1  or y2 < 0 or y2 > height-1:\n",
    "            continue\n",
    "        n.append((x2, y2))\n",
    "    return n\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:25.613460Z",
     "start_time": "2020-07-28T19:57:25.603462Z"
    }
   },
   "outputs": [],
   "source": [
    "def AStarSearch(start, end, graph, diameter, width, height):\n",
    " \n",
    "    G = {} #Actual movement cost to each position from the start position\n",
    "    F = {} #Estimated movement cost of start to end going via this position\n",
    " \n",
    "    #Initialize starting values\n",
    "    G[start] = 0 \n",
    "    F[start] = heuristic(start, end)\n",
    " \n",
    "    closedVertices = set()\n",
    "    openVertices = set([start])\n",
    "    cameFrom = {}\n",
    "    \n",
    "    # Adding a stop condition\n",
    "    outer_iterations = 0\n",
    "    max_iterations = (len(graph[0]) * len(graph) // 2)\n",
    " \n",
    "    while len(openVertices) > 0:\n",
    "        outer_iterations += 1\n",
    "\n",
    "        if outer_iterations > max_iterations:\n",
    "            warn(\"number of iterations has exceeded max iterations\")\n",
    "            \n",
    "        #Get the vertex in the open list with the lowest F score\n",
    "        current = None\n",
    "        currentFscore = None\n",
    "        for pos in openVertices:\n",
    "            if current is None or F[pos] < currentFscore:\n",
    "                currentFscore = F[pos]\n",
    "                current = pos\n",
    " \n",
    "        #Check if we have reached the goal\n",
    "        if current == end:\n",
    "            #Retrace our route backward\n",
    "            path = [current]\n",
    "            while current in cameFrom:\n",
    "                current = cameFrom[current]\n",
    "                path.append(current)\n",
    "            path.reverse()\n",
    "            return path  #Done!\n",
    " \n",
    "        #Mark the current vertex as closed\n",
    "        openVertices.remove(current)\n",
    "        closedVertices.add(current)\n",
    " \n",
    "        #Update scores for vertices near the current position\n",
    "        for neighbour in get_vertex_neighbours(current, diameter, width, height):\n",
    "            if neighbour in closedVertices: \n",
    "                continue #We have already processed this node exhaustively\n",
    "            x=neighbour[0]\n",
    "            y=neighbour[1]\n",
    "            if graph[x][y]!=150:\n",
    "                continue\n",
    "            else:\n",
    "                candidateG = G[current] + 1\n",
    " \n",
    "            if neighbour not in openVertices:\n",
    "                openVertices.add(neighbour) #Discovered a new vertex\n",
    "            \n",
    "            elif candidateG >= G[neighbour]:\n",
    "                continue #This G score is worse than previously found\n",
    " \n",
    "            #Adopt this G score\n",
    "            cameFrom[neighbour] = current\n",
    "            G[neighbour] = candidateG\n",
    "            H = heuristic(neighbour, end)\n",
    "            F[neighbour] = G[neighbour] + H\n",
    " \n",
    "    raise RuntimeError(\"A* failed to find a solution\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:25.821071Z",
     "start_time": "2020-07-28T19:57:25.814672Z"
    }
   },
   "outputs": [],
   "source": [
    "def AstarPath(M, Memory, path):\n",
    "    for q in path:\n",
    "        x=q[0]\n",
    "        y=q[1]\n",
    "        Memory.append((x, y))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Boustrophedon and caller"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:27.095795Z",
     "start_time": "2020-07-28T19:57:27.073119Z"
    }
   },
   "outputs": [],
   "source": [
    "def is_valid_vectorized(coords, matrix):\n",
    "    # Bound check\n",
    "    assert coords.shape[1] == 2\n",
    "    h,w = matrix.shape\n",
    "    x,y = coords.T\n",
    "    is_within_bounds = (x >= 0) & (x < h) & (y >= 0) & (y < w)\n",
    "    x = np.clip(x.copy(), 0, h-1)\n",
    "    y = np.clip(y.copy(), 0, w-1)\n",
    "    is_not_on_obstacle = (matrix[x,y] != 0) &  (matrix[x,y] != 150)\n",
    "    return is_within_bounds & is_not_on_obstacle\n",
    "\n",
    "def backtracking_list(memory, _, matrix, x, y):\n",
    "    bt_cond_points = {\n",
    "        \"r\":  lambda p: p + np.array([[0,1]]), # right\n",
    "        \"tr\": lambda p: p + np.array([[-1,1]]), # top-right\n",
    "        \"t\":  lambda p: p + np.array([[-1,0]]), # top\n",
    "        \"tl\": lambda p: p + np.array([[-1,-1]]), # top-left\n",
    "        \"l\":  lambda p: p + np.array([[0,-1]]), # left\n",
    "        \"bl\": lambda p: p + np.array([[1,-1]]), # bottom-left\n",
    "        \"b\":  lambda p: p + np.array([[1,0]]), # bottom\n",
    "        \"br\": lambda p: p + np.array([[1,1]]), # bottom-right\n",
    "    }\n",
    "    memory_ = np.array(memory)\n",
    "    assert memory_.shape[1] == 2, \"you've messed up something\"\n",
    "    \n",
    "    eight_di = {k: bt_cond_points[k](memory_) for k in bt_cond_points}\n",
    "    is_valid_eight = {k: is_valid_vectorized(eight_di[k], matrix) for k in eight_di}\n",
    "    cond_a = np.int0(is_valid_eight[\"r\"] & ~is_valid_eight[\"br\"])\n",
    "    cond_b = np.int0(is_valid_eight[\"r\"] & ~is_valid_eight[\"tr\"])\n",
    "    cond_c = np.int0(is_valid_eight[\"l\"] & ~is_valid_eight[\"bl\"])\n",
    "    cond_d = np.int0(is_valid_eight[\"l\"] & ~is_valid_eight[\"tl\"])\n",
    "    cond_e = np.int0(is_valid_eight[\"b\"] & ~is_valid_eight[\"bl\"])\n",
    "    cond_f = np.int0(is_valid_eight[\"b\"] & ~is_valid_eight[\"br\"])\n",
    "    μ_of_s = (cond_a + cond_b+ cond_c + cond_d + cond_e + cond_f)\n",
    "     \n",
    "    backtrack_points =  memory_[μ_of_s > 0]\n",
    "    if backtrack_points.shape[0] == 0:\n",
    "        backtrack_points = memory_[is_valid_eight[\"r\"] | is_valid_eight[\"l\"] |\n",
    "                is_valid_eight[\"t\"] | is_valid_eight[\"b\"]]\n",
    "        \n",
    "    if backtrack_points.shape[0] == 0:\n",
    "        return (0,0), True\n",
    "    else:\n",
    "        closest_point_idx = ((backtrack_points - np.array([x,y]))**2).sum(axis = 1).argmin()\n",
    "        return tuple(backtrack_points[closest_point_idx]), False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T18:28:04.759309Z",
     "start_time": "2020-07-28T18:28:04.668414Z"
    },
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.72 ms ± 388 µs per loop (mean ± std. dev. of 5 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit -n 10 -r 5 bt_point = backtracking_list(coverage_path, None,area_map, 0,0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T18:32:34.950510Z",
     "start_time": "2020-07-28T18:32:34.937690Z"
    }
   },
   "outputs": [],
   "source": [
    "def backtracking_list_nv(memory, _, matrix, x, y):\n",
    "    # Non vectorised implementation, runs faster; contig array alloc takes time!?\n",
    "    bt_cond_points = {\n",
    "        \"r\":  lambda x, y: (x + (+0), y + (+1)), # right\n",
    "        \"tr\": lambda x, y: (x + (-1), y + (+1)), # top-right\n",
    "        \"t\":  lambda x, y: (x + (-1), y + (+0)), # top\n",
    "        \"tl\": lambda x, y: (x + (-1), y + (-1)), # top-left\n",
    "        \"l\":  lambda x, y: (x + (+0), y + (-1)), # left\n",
    "        \"bl\": lambda x, y: (x + (+1), y + (-1)), # bottom-left\n",
    "        \"b\":  lambda x, y: (x + (+1), y + (+0)), # bottom\n",
    "        \"br\": lambda x, y: (x + (+1), y + (+1))  # bottom-right\n",
    "    }\n",
    "    \n",
    "    μ_of_s = []; \n",
    "    for point in memory:\n",
    "        av = { k:is_valid(bt_cond_points[k](*point), matrix, [0,150]) for k in bt_cond_points }\n",
    "        μ_of_s.append(int(av[\"r\"] and  not av[\"br\"]) + int(av[\"r\"] and  not av[\"tr\"]) + \n",
    "                      int(av[\"l\"] and  not av[\"bl\"]) + int(av[\"l\"] and  not av[\"tl\"]) + \n",
    "                      int(av[\"b\"] and  not av[\"bl\"]) + int(av[\"b\"] and  not av[\"br\"]))\n",
    "    μ_of_s = np.array(μ_of_s); # backup = np.array(backup);\n",
    "    memory_ = np.array(memory)\n",
    "    \n",
    "    backtrack_points =  memory_[μ_of_s > 0]\n",
    "    if backtrack_points.shape[0] == 0:\n",
    "        return (0,0), True\n",
    "    else:\n",
    "        closest_point_idx = ((backtrack_points - np.array([x,y]))**2).sum(axis = 1).argmin()\n",
    "        return tuple(backtrack_points[closest_point_idx]), False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 107,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T18:31:42.799842Z",
     "start_time": "2020-07-28T18:31:38.923013Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "77.5 ms ± 3.05 ms per loop (mean ± std. dev. of 5 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit -n 10 -r 5 _ = backtracking_list(coverage_path, None,area_map, 0,0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:32.566578Z",
     "start_time": "2020-07-28T19:57:32.558475Z"
    }
   },
   "outputs": [],
   "source": [
    "def visit(matrix, x, y, memory):\n",
    "    matrix[(x,y)] = 150 # 150 == visited\n",
    "    memory.append((x, y))\n",
    "    return x,y\n",
    "\n",
    "def boustrophedon(matrix, diameter, x, y, memory):\n",
    "    # TODO :: Variable diameter support\n",
    "    udlr = {\n",
    "        \"u\": lambda x,y : (x-diameter,y),\n",
    "        \"d\": lambda x,y : (x+diameter,y),\n",
    "        \"l\": lambda x,y : (x,y-diameter),\n",
    "        \"r\": lambda x,y : (x,y+diameter)\n",
    "    }\n",
    "    u = \"u\";d = \"d\";r = \"r\";l = \"l\"\n",
    "    visit(matrix, x, y, memory)\n",
    "    \n",
    "    while True:\n",
    "        dir_ = [u,d,r,l]\n",
    "        while len(dir_) > 0:\n",
    "            d_ = dir_.pop(0)\n",
    "            x_, y_ = udlr[d_](x,y)\n",
    "            if is_valid((x_,y_), matrix, [0, 150]):\n",
    "                x, y = visit(matrix, x_, y_, memory)\n",
    "                break\n",
    "            elif d_ == l:\n",
    "                return x, y"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:33.661756Z",
     "start_time": "2020-07-28T19:57:33.644878Z"
    }
   },
   "outputs": [],
   "source": [
    "def bous_preprocess(area_map):\n",
    "    \"\"\"\n",
    "    Returns matrix that in the form\n",
    "    that is read by the main algorithm.\n",
    "    \"\"\"\n",
    "    matrix = np.full(area_map.shape, 0, dtype=np.uint8)\n",
    "    matrix[area_map == 0] = 255\n",
    "    return matrix\n",
    "\n",
    "def do_everything(matrix, start_point, break_after_bous=False, timeit=True):\n",
    "    t = lambda :timeit and time.time()\n",
    "    times = {\n",
    "        \"bous\": [],\n",
    "        \"btrack\":[],\n",
    "        \"astar\":[]\n",
    "    }\n",
    "    store_time = lambda name,value: timeit and times[name].append(value)\n",
    "    start_time = t()\n",
    "    \n",
    "    width, height = matrix.shape\n",
    "    radius=0.5\n",
    "    diameter=int(2*radius)\n",
    "\n",
    "    x, y = start_point\n",
    "    memory=[]\n",
    "    \n",
    "    backtrack_counts = 0\n",
    "    point_find_failed = 0 \n",
    "\n",
    "    while True:\n",
    "        sw = t()\n",
    "        critical_x, critical_y = boustrophedon(matrix, diameter, x, y, memory)\n",
    "        store_time(\"bous\", t() - sw)\n",
    "        \n",
    "        if break_after_bous:\n",
    "            print(\"break point\",critical_x,critical_y)\n",
    "            break\n",
    "            \n",
    "        sw = t()\n",
    "        next_, is_end = backtracking_list(memory, diameter, matrix, critical_x, critical_y)\n",
    "        x,y = next_\n",
    "        store_time(\"btrack\", t() - sw)\n",
    "        if is_end:\n",
    "            break\n",
    "        else:\n",
    "            start = (critical_x,critical_y )\n",
    "            end = (x, y)\n",
    "            sw = t()\n",
    "            path = AStarSearch(start, end, matrix, diameter, width, height)\n",
    "            AstarPath(matrix, memory, path)\n",
    "            store_time(\"astar\", t() - sw)\n",
    "            \n",
    "    times_avg = timeit and {k+\"_avg\":np.array(times[k]).mean() for k in times}\n",
    "    times_tot = timeit and {k+\"_tot\":np.array(times[k]).sum() for k in times}\n",
    "    times = timeit and {**times_avg, **times_tot}\n",
    "    end_time = timeit and (time.time() - start_time)\n",
    "    if timeit: times['total'] = end_time\n",
    "    timeit and printer(times)\n",
    "    return memory "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:34.664244Z",
     "start_time": "2020-07-28T19:57:34.558935Z"
    }
   },
   "outputs": [],
   "source": [
    "area_maps = get_all_area_maps(\"./test_maps/\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:35.785829Z",
     "start_time": "2020-07-28T19:57:35.530686Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "times ↓\n",
      "            bous_avg : 0.0003654703180840675\n",
      "          btrack_avg : 0.0012489785539343\n",
      "           astar_avg : 0.0014533841091653576\n",
      "            bous_tot : 0.017177104949951172\n",
      "          btrack_tot : 0.05870199203491211\n",
      "           astar_tot : 0.06685566902160645\n",
      "               total : 0.1430509090423584\n",
      "\n",
      "metrics ↓\n",
      "     points_to_visit : 772\n",
      "     obstacle_points : 252\n",
      "      points_visited : 772\n",
      "   coverage_path_len : 1101\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.42616580310880825\n",
      "          area_shape : (32, 32)\n",
      "\n"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAcEAAAHBCAYAAAARuwDoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+j8jraAAAgAElEQVR4nO3dz2ulV57f8a90VeVbqqILi8ZYKbdBEoiOR17IztBlhhBCfiyKjoNpmN2AIfkLmjBbdyez8iIDgWxD7UIgoUgneDchm8aVha1mrLh7BKWKLHusFp5qV1OWr13PI2VRo5m51aX74/M8z/c533Per02ga07O0XnOuV9fXd3Pd+Hs7MwAACjRYt8LAACgLxRBAECxKIIAgGJRBAEAxaIIAgCKRREEABRradI/Lmy86fb9ifrnx15TAZ0Y/MELfS8hKdxptE29Y2f3frZw0b/xThAAUCyKIACgWBRBAECxKIIAgGIlXwRPT0/twW+qucednZ3Z11+fdj7Ge1yEueq6ts8+/3auMepz9hynzoVxqZ97dVyuc6nj1Lm879nEvw7t2+npqf3xTz+x9z94ZG+8fs3efedlW1ycXrfPzs7sv/3PB/bhR1/Za69etR/9cMUWFi784yB5jPe4CHPVdW1vvb1nO7sntr21bHdub9pgMJg4Rn3OnuPUuTAu9XMfYY0570cf9yzpW/zlw1N7/4NH9vJLz9n7HzyyLx/O9l8Vo9GZffjRV/a9v/ecffjRVzYaTf+mhzLGe1yEuY6Oa9vZPbEXX7hkO7sndnRcTx2jPmfPcepcGJf6uY+wxpz3o497lnQRXHl+yd54/Zp98uk39sbr12zl+dneuF65smivvXrVDv/yG3vt1at25cr0H1MZ4z0uwlw3Vi/b9tayHR0/tu2tZbuxennqGPU5e45T58K41M99hDXmvB993LOFSf0EU/iy/OnpqX358HTuzTg7O7PR6GzmzVfHeI+LMFdd13Z0XM9UAM+pz9lz3LQxfFl+3EV3OvVzH2GNOe/HpHvWxZflky+CQBQUwXHcabSNxBgAAFpEEQQAFIsiCAAoFn/iBhSEz+kQWRfnl3eCAIBiUQQBAMWiCAIAipV8ESRAm7lSGafOFUHqzyzn85H6XOo4zwD+Jndz8JOf/OTCf/zpf/jPF/9jy975V1/9zv92Hqb67n/8S/vlX5zYP/1H35kr8PW//Pe/st98Wdnf37wyc+DrPGO8xzFX2mv8t//p6tS5+/ase5b6M8vlfEScy3uN5wH8f/Knn9n//vlD+8N/uTI1QHuWuRau/eufXjQ+6XeCBGgzVyrj1LkiSP2Z5Xw+Up/Le41KAH/Tu5l0ESRAm7lSGafOFUHqzyzn85H6XN5rVAL4m97N5LNDCdBmrlTGTRsTITs0aqh1Ducj6lzea1QC+KfNtfjiXQK0ga5FLoJAziYVwXx+pwMAwJwoggCAYlEEAQDF6iRAm88dAAAR8E4QAFAsiiAAoFgUQQBAsSiCAIBiuRZBJelbSRRX5/JOZlc6ZKhdNVKfK+dU/AhSvy85n4/U51LHRZjLrKO/Dn2W86TvDz/6yl579ar96IcrU1PFzxPFd3ZPbHtr2e7c3rTBYNDJXMqYJuPOO2S8/8Eje+P1a/buOy9PTUtXxkSYy3vvPc9HBKnfl5zPR+pzRVhj07vp9k5QSfpWEsXVubyT2ZUOGWpXjdTnyjkVP4LU70vO5yP1uSKsMUwXCSXpW0kUV+fyTmZXOmSoXTVSnyvnVPwIUr8vOZ+P1OeKsMYku0i0mVSvJIqrc3knsysdMtSuGqnPlUMqfuQA7dTvSw7nI+pcEdaYXBcJEmNQoshFEMgZXSQAAHgGiiAAoFgUQQBAsSb+5QKfH/SHz5cwCXsPtIN3ggCAYlEEAQDFoggCAIolFcEIIc6eIaxqyLf6s6WOAO3+RLgvqc+ljst1LjPtNS7CfpgJAdoRQpw9Q1jVkG/1Z0sdAdr9iXBfUp8rwhq990N5jYuwH+fmftWNEOLsGcKqhnyrP1vqCNDuT4T7kvpcEdbovR/Ka1yE/Tg3dxGMEOLsGcKqhnyrP1vqCNDuT4T7kvpcEdbovR/Ka1yE/Tg3MTv09OjmM/8xQoizZ+CrGvI96WeL/D1BArS71+bem8UPSE5hXK5zmWmvcSnthxygfVERRPciF8FSpVAEAfwuArQBAHgGiiAAoFgUQQBAsfL4k8TEpf75Hp8vAd1L/XXArMzXAt4JAgCKRREEABSLIggAKBZFEABQLKkIeqaDR+hYkWs3CPU500WiP3SR6G9chPOhSv1Ou3aR8EwHj9CxItduEOpzpotEf+giEW+NEaR+p927SHimg0foWJFrNwj1OdNFoj90kYi3xghSv9PuXSQ808EjdKzItRuE+pzpItEfukjEW2MEqd/pXrpIeKaDR+hYMW1c6l+SvegLsupzpotE9+gikd64lM6HKmpnGLpIJC71w19iSkQXUiiCSFfqrwNm+Z4rukgAAPAMFEEAQLEoggCAYuXxp4xOIvxOH7Hk+hmMtwh3k2edJt4JAgCKRREEABSLIggAKBZFEED6vnhs9otHT/5foEXJd5FQ56rr2j77/Nu5xuTaDUKl7KE6zrtLgOcaFZ4/lzqf2zO784Ut/P4vbOEPf2kLv/8LsztfzD1nyjw73lRVZffuj+Yel/qdzraLhDpXXdf21tt7trN7Yttby3bn9qYNBoOJY3LtBqFS9lAd590lwHONCs+fS53P7Zl98dgWfnzfFkanZuev3T++b2f/8LrZdy9NnS91nh1vqqqym7c+tr39kW2uD+3ue6/Y0tL0EpD6nc66i4Q619FxbTu7J/biC5dsZ/fEjo7rqWNy7QahUvZQHefdJcBzjQrPn0udz+2ZffqN2dO1bumv//cMeHa8OTisbG9/ZCvXB7a3P7KDw9neSaZ+p7PuIqHOdWP1sm1vLdvR8WPb3lq2G6uXp47JtRuEStlDdZx3lwDPNSo8fy51Prdn9tJzZk9/DFj99f+eAc+ONxtrQ9tcH9qDh7Vtrg9tY20407jU73T2XSTUueq6tqPjeuYXArP43SBUF32JV9lDdZx3l4Au1qicj7Y7eKjPLOnODne+sIUf33/yDrAyO/v3a2ZvfXfs/yTC3WyzQ4PaLaSqKjs4rGYugOdSv9N0kXAS4aIpSLJoR5tFEE/54vGTX4G+9NwzPwuMcDd51v2ZVATL/p0fgBi+eymLP4RBesr980cAQPEoggCAYhX569AInx+o+NyhuZzPB5rjjuWFd4IAgGJRBAEAxaIIAgCKlW2AtmfYcQTKfqhhuxGec+ph6d77oYxT9zD18HKVukblnql773lfIoS5m2UaoO0ZdhyBsh9q2G6E55x6WLr3fijj1D1MPbxcpa5RuWfq3nvelwhh7ueyDND2DDuOQNkPNWw3wnNOPSzdez+Uceoeph5erlLXqNwzde8970uEMPdzWQZoe4YdR6Dshxq2G+E5px6W7r0fyjh1D1MPL1epa1Tumbr3nvclQpj7uWwDtCeNy/l7YG0GMqthu6k850kmBRB7no+2A7Q9x6khzqmEl6vafmbKPVP3vov7cpGUwtwJ0H5KiUUQs0vhBRXz4ZlhkklFML3fKwAA4IQiCAAoFkUQAFCstP4sDn+Dzx3i4Zk1l/Pn9UgT7wQBAMWiCAIAikURBAAUiyIIACgWXSSCUdPjlXGeSfU5d5FQ5sq5iwR3s/m4CPclwn6Y0UUiFDU9XhnnmVSfcxcJZa6cu0hwN5uPi3BfIuzHObpIBKKmxyvjPJPqc+4iocyVcxcJ7mbzcRHuS4T9OEcXiUDU9HhlnGdSfc5dJJS5cu4iwd1sPi7CfYmwH+foIpGoi754rabHK+M8k+pz6CLR5jPLuYsEd7P5uJTuS5tj1HF0kZhT5IuG7rVdBDE77ia6QBcJAACegSIIACgWRRAAUKzwXST4DAGTRDgfuWLvEQHvBAEAxaIIAgCKRREEABTLNUC7rmv77PNv5xrjGXTsTfnZqqqye/dHLnMRlN6O1PdeHZfz3VQQON/fXE3OoluAdl3X9tbbe7aze2LbW8t25/amDQaDiWM8g469KT9bVVV289bHtrc/ss31od197xVbWpr+CFMP6c05KD31vVfH5Xw3FQTO9zdX07PoFqB9dFzbzu6JvfjCJdvZPbGj43rqGM+gY2/Kz3ZwWNne/shWrg9sb39kB4ez/ZdP6iG9OQelp7736ric76aCwPn+5mp6Ft0CtG+sXrbtrWU7On5s21vLdmP18tQxnkHH3pSfbWNtaJvrQ3vwsLbN9aFtrA07m4ug9HakvvfquJzvpoLA+f7manoWXQO067q2o+N6pgL4N2uYEqYa4btIbQbuVlVlB4fVzAWwyVzRg9LNfM8HAdrjItxNRdsB2indl4t4nuEuwrqzDtCOcNH4snx/UiiCpYpwNxU853gI0AYA4BkoggCAYlEEAQDFmvjJY66/01fxWUAZeM7l4Fk3F6FOnN27+N94JwgAKBZFEABQLIogAKBYFEEAQLEogi3w7KoRobODkuiu7IWZbxeJXJ+zOi5CBw+VZ8ebXLtIRFF24F8LPLtqROjsoCS6K3vRZI2KXJ+zOi5CBw+VZ8ebXLtIRJLPT9ITz64aETo7KInuyl40WaMi1+esjovQwUPl2fEm1y4SkVAEG/LsqhGhs4OS6K7sRZM1KnJ9zuq4CB08VJ4db3LtIhHJxADthY038/nPuxZc9MVaz64aETo7KCnwyl7Mskbli7ylPWd1XEodPFQXPesuOt5cJHoXiQjP+ezezy78vW9eJb0nCwsLduXK/J+HDAYDu7E6/bOvNuZSxqlzLS4u2srz810yZS/M9DUqcn3O6jjPvfemPDPl3Jv57r3nnY4iz58KAIAZUAQBAMWiCAIAijXxM0HPhHU6gGOSCB++oz/c6XaUeM94JwgAKBZFEABQLIogAKBYFEEAQLGkIuidYO7FMwVeHRdhLmUfUz8bZvnuvZlvpwtP6hqrqrJ790cuc6X+OpC7uRNjvBPMvXimwKvjIsyl7GPqZ8Ms37038+104UldY1VVdvPWx7a3P7LN9aHdfe8VW1qa/FIZ4XxEeGZ9mPudoHeCuRfPFHh1XIS5lH1M/WyY5bv3Zr6dLjypazw4rGxvf2Qr1we2tz+yg8Pp76wjnI8Iz6wPcxdB7wRzL54p8Oq4CHMp+5j62TDLd+/NfDtdeFLXuLE2tM31oT14WNvm+tA21oadzZX660AJJnaROD26+cx/7CLBPIUvy3umwKvjIsyl7GNKHQna7iKR+t6bddPpIvIzq6rKDg6rmQpg07lSeh3I9cvyrXeR8E4w9+KZAq+OizCXso+pnw2zfPfezLfThSd1jUtLS7axNt/LY4TzEeGZeeP9MACgWBRBAECxKIIAgGKF7yxPenw8uX74niueF3LGO0EAQLEoggCAYlEEAQDFkoqgGtKrjlMoQbFKiLA6lzrOM1hZ3Q/P5+wp1+fcZL7URQiBJ0C7X3P/YYwa0quOUyhBsUqIsDqXOs4zWFndD8/n7CnX59xkvtRFCIEnQLt/c786qSG96jiFEhSrhAirc6njPIOV1f3wfM6ecn3OTeZLXYQQeAK0+zd3EVRDetVxCiUoVgkRVudSx3kGK6v74fmcPeX6nJvMl7oIIfAEaPdPCtBWQ3onjVO/i9RmcK4SIqzOpY7zDFZW92PaXKl/76zEAO1Uwu1VbT+zLkLg2xxHgPZ8Wg/QVkN61XEKJShWCRFW51LHeQYrq/vh+Zw95fqcm8yXuggh8ARo9yu/VyoAAGZEEQQAFIsiCAAoVid/upfrh6sYF+E5E7AOzC71+9LFaw7vBAEAxaIIAgCKRREEABSLIggAmOhre85+vbBiX9tzfS+lda5dJDxFSGZPvbtAzonzuXYJoIvEuAj3JfXXqr9YfNluX37T7lz6x3b78pu2t/hycmtswq2LhKcIyeypdxfIOXE+1y4BdJEYF+G+pP5a9bU9Z/9r6QdWLfxtqfizpR/Y9779tV2xb5JYY1NuXSQ8RUhmT727QM6J87l2CaCLxLgI9yX116rfLly1BRv/+Rfs1H67cDWZNTbl1kXCU4Rk9tS7C+ScOJ9rlwC6SIyLcF9Sf636ztlXdvZUmTizRfvO2VfJrLGpTrpIeH6Jus30eM8UeO81dpGKH/nL8rl2CaCLxLiU7kub4zzn2lt82f5s6Qe2YKd2Zov2T6r/Y5unn/SyRvUsJtNFwlOEZPbUuwvknDifa5cAukiMi3BfUn+t2jz9xL737a/ttwtX7TtnX038LLCvNTaR3u8yAQBJuWLf2JWz2YpfNGm/nQMAoEMUQQBAsTr5daiSRB7hw/dcsffl4FkD43gnCAAoFkUQAFAsiiAAoFhSEazr2j77/Nu5x3kGo6YeSquOU/c+Qui5p1wDknnO4wjQ7m9ctgHadV3bW2/v2c7uiW1vLdud25s2GAymjvMMRk09lFYdp+59hNBzT7kGJPOcxxGgXc4am5j7hhwd17aze2IvvnDJdnZP7Oi4nmmcZzBq6qG06jh17yOEnnvKNSCZ5zyOAO1y1tjE3EXwxupl295atqPjx7a9tWw3Vi/PNM4zGDX1UFp1nLr3EULPPeUakMxzHkeAdjlrbEIK0K7r2o6O65lfhM91EYxaWoC2uveTAoFz/u4YAdrjcn3WBGinNy7rAO3BYGA3Vqd/FvU0z2DU1ENp1XHq3kcIPfeUa0Ayz3kcAdr9jYsSoM1tAQAUiyIIACgWRRAAUKyy/3wsQ6X94QMANME7QQBAsSiCAIBiUQQBAMWiCAIAiiUVwQjp4BFSz5VxdAkYp+690o3DsyNBhE4tEaj3xfN85PpaFaFOmAl/HRohHTxC6rkyji4B49S9V7pxeHYkiNCpJQL1vniej1xfqyLUiXNzv4JGSAePkHqujKNLwDh175VuHJ4dCSJ0aolAvS+e5yPX16oIdeLc3EUwQjp4hNRzZRxdAsape6904/DsSBChU0sE6n3xPB+5vlZFqBPnpC4SKaWDl9ZFgi4B49S9V7pxeHYk6KJTi1l550PZezPf85Hra1VKdaL1LhIR0sEjpJ4r4+gSME7de6Ubh2dHggidWiJQ74vn+cj1tSpCnTDjKxIAgIJRBAEAxaIIAgCKVfafFyYs1z9gQDva/kOyVKg/V677ge7xThAAUCyKIACgWBRBAECxKIIAgGLRRaLhmD7G4W9FSMVXOhl4n40I98UTXSSaj4tyPugi0XB9EfYjVxFS8ZVOBt5nI8J98UQXiXhrbIIuEg3XF2E/chUhFV/pZOB9NiLcF090kYi3xiboItFwfRH2I1cRUvGVTgbeZyPCffFEF4l4a2yCLhINx3Q1ji/Lj2u7i4Tn+VA6GXTVDSL1++J97i/aD7pIpLlGukh0PFeEZHaMi5CKr3Qy8D4bEe6LJ7pINB8X5Xyk97sIAACcUAQBAMUiQBvuCC2OhXBq5Ix3ggCAYlEEAQDFoggCAIpFEQQAFIsA7YZj+hiXIyVk2ixGIHCuAdoR5lIRoN18XJTXRQK0G64vwn6kTgmZNosRCJxrgHaEuVQEaMdbYxMEaDdcX4T9SJ0SMm0WIxA41wDtCHOpCNCOt8YmCNBuuL4I+5E6JWTaLEYgcK4B2hHmUhGgHW+NTRCg3XBMV+NyDtB+1jNTQqbNYgQCRw/QjjCXigDtWGskQLvjuSKE0uZKCZk2ixEInGuAdoS5VARoNx8X5XUxvd9FAADghCIIACgWRRAAUCy6SCSKBH5gdtwXqHgnCAAoFkUQAFAsiiAAoFgUQQBAsegi0XCM97hc51JS+9W51HF0kYg3l9qdpKoqu3d/NNeYCGdRHafcT3Xv1XEqukg0XF+uyeyecymp/d5rpItEvLnU7iRVVdnNWx/b3v7INteHdve9V2xpafJLZYSzqI5T7qe69+q4Jugi0XB9uSaze86lpPZ7r5EuEvHmUruTHBxWtrc/spXrA9vbH9nB4fR3JRHOojpOuZ/q3qvjmqCLRMP15ZrM7jmXktrvvUa6SMSbS+1OsrE2tM31oT14WNvm+tA21oZTx0Q4i+o45X6qe6+Oa4IuEg3HeI/LdS4ltd97jXSRiDeX2p2kqio7OKxmKoDnIpxFdZxyP9W9nzSOLhIdz0Uye39zKan96lzqOLpIxJtL7U6ytLRkG2vzvTxGOIvqOOV+qnuvjlPxFQkAQLEoggCAYlEEAQDFoosE0KO2/xCkC7nOhX6pZ79tvBMEABSLIggAKBZFEABQLKkIRghGjRBK6xmsnPp+RNh7NeQ79XNvpgVGK2PMzB4/fmz/91cnc41R997zmXm+Lnq/BnveaW9z/2FMhGDUCKG0nsHKqe9HhL1XQ75TP/dmWmC0MsbsSQH8/h98ZJ9+/theWr1kv/r5q3bp0qWJY9S993xmnq+L3q/Bnne6D3PfxgjBqBFCaT2DlVPfjwh7r4Z8p37uzbTAaGWMmdnevcf26eeP7drygn36+WPbu/d46hh17z2fmefrovdrsOed7sPcRTBCMGqEUFrPYOXU9yPC3qsh36mfezMtMFoZY2b2e99ftpdWL9mjkzN7afWS/d73l6eOUffe85l5vi56vwZ73uk+SAHaKQWjRg7Q7iJYuc1xuc5lpu29GvKd+rk30wKjlTFmT34lunfv8UwF8Jy69108szbHRJjLrJs77fk9wUkB2lIR7EKELw0DbePco1SpFMF036MCANAxiiAAoFgUQQBAsSZ+OsrnFcDslPvCXUFKUgm19sQ7QQBAsSiCAIBiUQQBAMWiCAIAiuVaBFNP0/fsSKCOy7WzQ4QuEp6p+J5p/+p8OZ+P1NeodseI0tnBU3dBhk9JPU3fsyOBOi7Xzg4Rukh4puJ7pv2r8+V8PlJfo9odI1JnB09u7wRTT9P37Eigjsu1s0OELhKeqfieaf/qfDmfj9TXqHbHiNTZwZNbEUw9Td+zI4E6LtfODhG6SHim4num/avz5Xw+Ul+j2h0jUmcHTxMDtBc23pT+U+GiLwCnnqbv2ZFAHZdrZ4cIXSS6SMVX7kqTNV6EsxhrjWp3jJQ6O3iaFKDt9pmgmdni4qKtPO/zXx8LCwt25cp8v+9WxpjpP5cyTl2j536kPpeZ794r1DMVYT8inI/U1zgYDOzG6vTPAduYK3e8HwYAFIsiCAAoFkUQAFCsiZ8JknCPEuX6xwEoB2d4drwTBAAUiyIIACgWRRAAUCyKIACgWFIRVJPI1eRzhZKKryb3V1Vl9+6P5h7nucZc51L33vMsKugi0d9c6rgI9wW/a+4ieJ5E/u/+9FP7r//jr2xS7NrfdZ58fvPWR/bmH/3S6nq20FfFeSr+v/ijX9m/eef/2enp9MOsjDF78iJ889bHtv3P/tz+wT//c6uq2Q6m5xpznUvde8+zqFD3Q72bynzqXMo4z7nUcRHuC55t7iKoJpGryecKJRVfTe4/OKxsb39kK9cHtrc/soPD2V6IPdeY61zq3nueRQVdJPqbSx0X4b7g2eYugmoSuZp8rlBS8dXk/o21oW2uD+3Bw9o214e2sTZMbo25zqXuvedZVNBFor+51HER7guebWIXidOjm8/8RzUtfVLyedtdJJRUfDW5v6oqOzisZn4R7mONuc6l7n0XZ1FBF4n05lLHpXRf+LL8uEldJKQi2IW2iyCgSqEIAk1QBMdNKoJ8RQIAUCyKIACgWOE/UeVtP4AIIrxWpf7r+S72kHeCAIBiUQQBAMWiCAIAikURBAAUyzVAWx0HlCbnAO3Uw9zVcTmHWkd4Ziq3AG11HFCanAO0Uw9z915jBBGeWRNuAdrqOKA0OQdopx7m7r3GCCI8sybcArTVcUBpcg7QTj3M3XuNEUR4Zk24BmhPGhfhi6RA20oM0E49zL2rNUZ4jUuhIcGkceoeTsoOlcrswsKCXbly4f+frY8DSrO4uGgrz8//2xL1jinzec6l7ofnOHWuCCI8M1WeTwwAgBlQBAEAxaIIAgCKlc+fMCUs9WR2bxH+QAD94GzE49kQvYvXUt4JAgCKRREEABSLIggAKBZFEABQLLpI9MgzuV8Zx3PuD10kkBLPZ+bdwWPuvw49T4//8KOv7LVXr9qPfrhiCwvTUyPUcbk6T0t//4NH9sbr1+zdd162xcXJ/03iufc85/4oZ8NM33vPs6j+bOiP5zNT5mq6PrpI9MQzuV8Zx3PuD10kkBLPZ9ZHBw+6SPTEM7lfGcdz7g9dJJASz2fWRwcPukg4aDOZvYu9954r52c9L7pIjONs9Es5j21/Wb6LjhWLL96li0SKPJP7lXE85/7QRQIp8Xxm3h08OIkAgGJRBAEAxaIIAgCKxZ9mzYFuEO3w3Ef+0AJIk3I36SIBAECLKIIAgGJRBAEAxSJAuwXqz1XXtX32+bcuc6UeoM2ZGkeANlKS+jMjQLtH6s9V17W99fae7eye2PbWst25vWmDwaCTuVIP0OZMjSNAGylJ/ZkRoN0z9ec6Oq5tZ/fEXnzhku3sntjRcd3ZXKkHaHOmxhGgjZSk/swI0O6Z+nPdWL1s21vLdnT82La3lu3G6uXO5ko9QJszNY4AbaQk9WdGgLaji76jou5HXdd2dFzPVACbzpVSgLbnXKmfKwK0x6X+vHLnGaCtUO8LAdodU3+uwWBgN1Ynfw7Y1lypB2hzpsYRoI2UpP7MCNAGAEBAEQQAFIsiCAAoVjJ/5qMGo/JBOiZRzhVnqj+E1JcjlWfNO0EAQLEoggCAYlEEAQDFoggCAIqVfBeJCF0CIuwHc7UzzgtdJJqP8R6X61xmZlVV2b37o7nHKTz3wyzxLhIRugRE2A/mamecF7pINB/jPS7XucyeFMCbtz62vf2Rba4P7e57r9jSUjdfLPDcj3NJd5GI0CUgwn4wVzvjvNBFovkY73G5zmVmdnBY2d7+yFauD2xvf2QHh931FfTcj3NJd5GI0CUgwn4wVzvjvNBFovkY73G5zmVmtrE2tM31oT14WNvm+tA21oYzjVN47se5ZLpIqGNSSDBPaT+Yq/m4FM5UqV0k2hzjPS7Xucye/Er04LB6ZgFU70ubZ79JFwmpCKYkhRcs5IUzBcyu7SLYhUlFMGcCqA8AAAfySURBVK3fAwEA4IgiCAAoFkUQAFCsZLpIAADiidAB6Ozexf/GO0EAQLEoggCAYlEEAQDFcg3Q9g5G9ZJr0HSEubyDpr3kHKCd+llUx+U6l5l2Puq6ts8+/3buuby5BWj3EYzqIdeg6QhzeQdNe8k5QDv1sxhhjd77oZyPuq7trbf3bGf3xLa3lu3O7U0bDAZT5+qDW4B2H8GoHnINmo4wl3fQtJecA7RTP4sR1ui9H8r5ODqubWf3xF584ZLt7J7Y0XE901x9cAvQ7iMY1UOuQdMR5vIOmvaSc4B26mcxwhq990M5HzdWL9v21rIdHT+27a1lu7F6eaa5+uAaoN1FMGoKOY+5Bk1HmKuLoOkUzlTOAdqpn8UIa/TeD+V81HVtR8f1hQXQ93uCPyNAuw2EHZeBMwV0L5UimNbvgQAAcEQRBAAUiyIIAChWen9uOSc+UwGAeJTX7i4+R+SdIACgWBRBAECxKIIAgGJRBAEAxXLtIqGMU9P0PTtWeI7LNameLhLjvM+ikvgf4XzkusYI+1FVld27P5p7Lu/OQW5dJJRxapq+Z8cKz3G5JtXTRWKc91lUEv8jnI9c1xhhP6qqspu3Pra9/ZFtrg/t7nuv2NLS9HLTR+cgty4Syjg1Td+zY4XnuFyT6ukiMc77LCqJ/xHOR65rjLAfB4eV7e2PbOX6wPb2R3ZwONs7uz46B7l1kVDGqWn6nh0rPMflmlRPF4lx3mdRSfyPcD5yXWOE/dhYG9rm+tAePKxtc31oG2vDmebqo3OQaxcJZZyapt9Fx4oUxuWaVE8XiXHeZ3Fa4v+zRDgfua4xwn5UVWUHh9XMBfDcpDOs3s2su0gAbUuhCAL4XV0UwbR+DwQAgCOKIACgWBRBAECxwneRACLz/PzRjM8ggafxThAAUCyKIACgWBRBAECxKIIAgGIl30Ui17nMfLtIpN5Vgy4S/aKLRPNxuc6ljvO+06qku0jkOpeZbxeJ1Ltq0EWiX3SRiLXGnPeDLhKFzGXm20Ui9a4adJHoF10kYq0x5/2gi0Qhc5n5dpFIvasGXST6RReJWGvMeT/oIlHQXGa+XSRS76pRahcJbxd9WZ4uErHWmPN+0EUC6FmJRRCIgC4SAAC0iCIIACgWRRAAUCy6SED+PTufL/WHvQfawTtBAECxKIIAgGJRBAEAxSJAu6e5zLSgWCXoWJ1LRYB2f9T9qKrK7t0fzTUm57uZ+hpz3g8CtAuYy0wLilWCjtW5VARo90fdj6qq7Oatj21vf2Sb60O7+94rtrQ0+aUh57uZ+hpz3g8CtAuZy0wLilWCjtW5VARo90fdj4PDyvb2R7ZyfWB7+yM7OJz+X+E5383U15jzfhCgXchcZlpQrBJ0rM6lIkC7P+p+bKwNbXN9aA8e1ra5PrSNteHUMTnfzdTXmPN+EKBd0FxmWvizEnQ8ba62vydIgHb32tx7sye/Ej04rGYqgOdyvpuprzHn/SBAG+74svy4yEUQyBkB2gAAtIgiCAAoFkUQAFAsArSBp/B5G1AO3gkCAIpFEQQAFIsiCAAoFkUQAFAsukj0NJc6LkIyu9LpQl2f2lVD6ZqgrlEZ5zmXWfr3Jee7mfpc6rgIr1VmdJHoZS7vNXomsyudLtT1qV01lK4J6hqVcZ5zmaV/X3K+m6nP5b1GukgUMpf3Gj2T2ZVOF+r61K4aStcEdY3KOM+5zNK/LznfzdTn8l4jXSQKmct7jZ7J7EqnC3V9alcNpWuCukZlnOdcZunfl5zvZupzea+RLhIFzeW9Rs8uEkqnC7X7gdpVQ+maoK5RGec5l1n69yXnu5n6XN5rpIsE3NFFAkAEdJEAAKBFFEEAQLEoggCAYtFFIjMRuqIDQCqvVbwTBAAUiyIIACgWRRAAUCwCtHuaSx2nzuVJCbXOee9Tn0sdl+tc6rhc5zLzvdPeCNDuYS7vNXpSQq1z3vvU54qwRvajv7nMfO90HwjQ7mEu7zV6UkKtc9771OeKsEb2o7+5zHzvdB8I0O5hLu81elJCrXPe+9TnirBG9qO/ucx873QfCNDuaa6u1uj53Zs2A7Rz2Puoc0VYI/vR31xm3dxpz9cqArQLkkIRBIBpUimC6b5HBQCgYxRBAECxKIIAgGIRoJ0oGt0CiCCVIGwV7wQBAMWiCAIAikURBAAUiyIIACgWXSRamEtJWDczOz09tQe/qeYep1DmUtdXVZXduz+aa4z3HirjIpzFXNfIfvQ3l5nva5U3ukg0nEtJWDd7cqj++Kef2PsfPLI3Xr9m777zsi0udvPGXJlLXV9VVXbz1se2tz+yzfWh3X3vFVtamnzMvPdQGRfhLOa6Rvajv7nMfF+r+kAXiYZzKQnrZmZfPjy19z94ZC+/9Jy9/8Ej+/Jhd323lLnU9R0cVra3P7KV6wPb2x/ZweH0/3r03kNlXISzmOsa2Y/+5jLzfa3qA10kGs6lJKybma08v2RvvH7NPvn0G3vj9Wu28nx3X9lU5lLXt7E2tM31oT14WNvm+tA21oZTx3jvoTIuwlnMdY3sR39zmfm+VvWBLhItzKUkrJs9+TXDlw9Pn3mo2v6y/KS5lPVNUlWVHRxWMxXAc13sYdvjIpzFXNfIfvQ3l1k3r1We6CIREIkxACKIXgTz+XQTAIA5UQQBAMWiCAIAijXxM0EAAHLGO0EAQLEoggCAYlEEAQDFoggCAIpFEQQAFIsiCAAo1v8H1BzN41s+dygAAAAASUVORK5CYII=\n",
      "text/plain": [
       "<Figure size 576x576 with 1 Axes>"
      ]
     },
     "metadata": {
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "area_map = area_maps[8]\n",
    "\n",
    "start_point = (12, 28)\n",
    "matrix = bous_preprocess(area_map)\n",
    "print(\"times ↓\")\n",
    "coverage_path = do_everything(matrix, start_point, break_after_bous=False)\n",
    "end_point = coverage_path[-1]\n",
    "\n",
    "print(\"\\nmetrics ↓\")\n",
    "printer(coverage_metrics(area_map, coverage_path))\n",
    "print()\n",
    "\n",
    "imshow(matrix, figsize=(8, 8), cmap=\"cividis\")                       #Shift + Tab\n",
    "imshow_scatter(coverage_path, alpha=0.4, color=\"black\",s=5)\n",
    "imshow_scatter([start_point],color=\"lightgreen\")\n",
    "imshow_scatter([end_point],color=\"red\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:38.318034Z",
     "start_time": "2020-07-28T19:57:38.307845Z"
    },
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "def cover_test(first_n_area_maps, times):\n",
    "    t_ = []\n",
    "    for j,area_map in tqdm(enumerate(area_maps[:first_n_area_maps]),total=first_n_area_maps):\n",
    "        t = []\n",
    "        for i in range(times):\n",
    "            start_point = get_random_coords(area_map, 1)[0]\n",
    "            matrix = bous_preprocess(area_map)\n",
    "            try:\n",
    "                s = time.time()\n",
    "                coverage_path = do_everything(matrix, start_point, timeit=False)\n",
    "                t.append(time.time() - s)\n",
    "            except:\n",
    "                print(f\"j: {j}, start: {start_point} FAILED\")\n",
    "                imshow(area_map)\n",
    "                return\n",
    "            end_point = coverage_path[-1]\n",
    "            metrics = coverage_metrics(area_map, coverage_path)\n",
    "            if metrics[\"coverage\"] < 1.0:\n",
    "                print(f\"j: {j}, start: {start_point}, end: {end_point}, cover:{metrics['coverage']} ,redun:{metrics['redundancy']}\")\n",
    "                print()\n",
    "        t_.append(np.array(t).mean())\n",
    "    return t_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:57:45.959707Z",
     "start_time": "2020-07-28T19:57:40.535366Z"
    }
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "705f1cc9fb2f4a5980590d46c08e58b8",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(FloatProgress(value=0.0, max=11.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/alan/opt/anaconda3/envs/data_sci/lib/python3.7/site-packages/ipykernel_launcher.py:22: UserWarning: number of iterations has exceeded max iterations\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "[0.032391834259033206,\n",
       " 0.022753334045410155,\n",
       " 0.029361772537231445,\n",
       " 0.052572011947631836,\n",
       " 0.028550124168395995,\n",
       " 0.028410506248474122,\n",
       " 0.026017594337463378,\n",
       " 0.08865394592285156,\n",
       " 0.06439673900604248,\n",
       " 0.06884322166442872,\n",
       " 0.08887407779693604]"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# If returns time without any print then SUCCESS!\n",
    "cover_test(11,10) # cover test for all the maps <= 50, 50"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-07-28T19:58:05.001381Z",
     "start_time": "2020-07-28T19:57:48.159885Z"
    },
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a53f03e5d27b491ab2f53e0799efca5f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(FloatProgress(value=0.0, max=21.0), HTML(value='')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "# 0 ::  (32, 32) corners_0.png\n",
      "            bous_avg : 0.0006066295835706922\n",
      "          btrack_avg : 0.0012082788679334852\n",
      "           astar_avg : 0.00030307208790498623\n",
      "            bous_tot : 0.010919332504272461\n",
      "          btrack_tot : 0.021749019622802734\n",
      "           astar_tot : 0.005152225494384766\n",
      "               total : 0.03796982765197754\n",
      "     points_to_visit : 716\n",
      "     obstacle_points : 308\n",
      "      points_visited : 716\n",
      "   coverage_path_len : 807\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.12709497206703912\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 2 ::  (256, 256) comb_9.png\n",
      "            bous_avg : 0.0012152791023254395\n",
      "          btrack_avg : 0.014023769927281205\n",
      "           astar_avg : 0.006197479535948555\n",
      "            bous_tot : 0.4520838260650635\n",
      "          btrack_tot : 5.216842412948608\n",
      "           astar_tot : 2.299264907836914\n",
      "               total : 7.97052001953125\n",
      "     points_to_visit : 33385\n",
      "     obstacle_points : 32151\n",
      "      points_visited : 33385\n",
      "   coverage_path_len : 38477\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.15252358843792124\n",
      "          area_shape : (256, 256)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 3 ::  (144, 256) comb_8.png\n",
      "            bous_avg : 0.003343343734741211\n",
      "          btrack_avg : 0.011790562211797478\n",
      "           astar_avg : 0.008448570966720581\n",
      "            bous_tot : 0.2975575923919678\n",
      "          btrack_tot : 1.0493600368499756\n",
      "           astar_tot : 0.7434742450714111\n",
      "               total : 2.0910308361053467\n",
      "     points_to_visit : 24720\n",
      "     obstacle_points : 12144\n",
      "      points_visited : 24720\n",
      "   coverage_path_len : 26085\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.05521844660194164\n",
      "          area_shape : (144, 256)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 4 ::  (32, 32) pipes_0.png\n",
      "            bous_avg : 0.0006602128346761067\n",
      "          btrack_avg : 0.0009100437164306641\n",
      "           astar_avg : 0.00024156911032540456\n",
      "            bous_tot : 0.009903192520141602\n",
      "          btrack_tot : 0.013650655746459961\n",
      "           astar_tot : 0.003381967544555664\n",
      "               total : 0.027039051055908203\n",
      "     points_to_visit : 689\n",
      "     obstacle_points : 335\n",
      "      points_visited : 689\n",
      "   coverage_path_len : 779\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.13062409288824384\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 5 ::  (32, 32) pipes_1.png\n",
      "            bous_avg : 0.0004187634116724918\n",
      "          btrack_avg : 0.0008177380812795539\n",
      "           astar_avg : 0.0001951720979478624\n",
      "            bous_tot : 0.007956504821777344\n",
      "          btrack_tot : 0.015537023544311523\n",
      "           astar_tot : 0.0035130977630615234\n",
      "               total : 0.027118206024169922\n",
      "     points_to_visit : 639\n",
      "     obstacle_points : 385\n",
      "      points_visited : 639\n",
      "   coverage_path_len : 764\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.19561815336463217\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 6 ::  (32, 32) pipes_2.png\n",
      "            bous_avg : 0.00033684730529785154\n",
      "          btrack_avg : 0.0008809757232666015\n",
      "           astar_avg : 0.001022746165593465\n",
      "            bous_tot : 0.008421182632446289\n",
      "          btrack_tot : 0.02202439308166504\n",
      "           astar_tot : 0.024545907974243164\n",
      "               total : 0.055140018463134766\n",
      "     points_to_visit : 654\n",
      "     obstacle_points : 370\n",
      "      points_visited : 654\n",
      "   coverage_path_len : 973\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.48776758409785925\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 7 ::  (32, 32) caves_1.png\n",
      "            bous_avg : 0.0013619661331176758\n",
      "          btrack_avg : 0.0010527968406677246\n",
      "           astar_avg : 0.002979346684047154\n",
      "            bous_tot : 0.010895729064941406\n",
      "          btrack_tot : 0.008422374725341797\n",
      "           astar_tot : 0.020855426788330078\n",
      "               total : 0.04027700424194336\n",
      "     points_to_visit : 794\n",
      "     obstacle_points : 230\n",
      "      points_visited : 794\n",
      "   coverage_path_len : 893\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.12468513853904284\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 8 ::  (32, 32) caves_0.png\n",
      "            bous_avg : 0.0005809980280259077\n",
      "          btrack_avg : 0.0010146533741670497\n",
      "           astar_avg : 0.0005776435136795044\n",
      "            bous_tot : 0.00987696647644043\n",
      "          btrack_tot : 0.017249107360839844\n",
      "           astar_tot : 0.00924229621887207\n",
      "               total : 0.03648209571838379\n",
      "     points_to_visit : 725\n",
      "     obstacle_points : 299\n",
      "      points_visited : 725\n",
      "   coverage_path_len : 896\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.2358620689655173\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 9 ::  (32, 32) center_0.png\n",
      "            bous_avg : 0.0007597666520338792\n",
      "          btrack_avg : 0.0009075494912954477\n",
      "           astar_avg : 0.00020899375279744467\n",
      "            bous_tot : 0.00987696647644043\n",
      "          btrack_tot : 0.01179814338684082\n",
      "           astar_tot : 0.002507925033569336\n",
      "               total : 0.0242922306060791\n",
      "     points_to_visit : 723\n",
      "     obstacle_points : 301\n",
      "      points_visited : 723\n",
      "   coverage_path_len : 798\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.10373443983402497\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 10 ::  (144, 256) center_1.png\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "/Users/alan/opt/anaconda3/envs/data_sci/lib/python3.7/site-packages/ipykernel_launcher.py:22: UserWarning: number of iterations has exceeded max iterations\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "            bous_avg : 0.028317672865731374\n",
      "          btrack_avg : 0.01696244307926723\n",
      "           astar_avg : 0.018626616551325872\n",
      "            bous_tot : 0.39644742012023926\n",
      "          btrack_tot : 0.2374742031097412\n",
      "           astar_tot : 0.24214601516723633\n",
      "               total : 0.8762199878692627\n",
      "     points_to_visit : 36713\n",
      "     obstacle_points : 151\n",
      "      points_visited : 36713\n",
      "   coverage_path_len : 37074\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.009833029172227725\n",
      "          area_shape : (144, 256)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 11 ::  (50, 144) comb_5.png\n",
      "            bous_avg : 0.0028441373039694395\n",
      "          btrack_avg : 0.0018088396857766545\n",
      "           astar_avg : 0.001951858401298523\n",
      "            bous_tot : 0.04835033416748047\n",
      "          btrack_tot : 0.030750274658203125\n",
      "           astar_tot : 0.031229734420776367\n",
      "               total : 0.11046028137207031\n",
      "     points_to_visit : 4383\n",
      "     obstacle_points : 2817\n",
      "      points_visited : 4383\n",
      "   coverage_path_len : 4620\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.054072553045859006\n",
      "          area_shape : (50, 144)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 12 ::  (50, 144) comb_4.png\n",
      "            bous_avg : 0.0014530294819882041\n",
      "          btrack_avg : 0.0016398429870605469\n",
      "           astar_avg : 0.0013732008031896643\n",
      "            bous_tot : 0.05521512031555176\n",
      "          btrack_tot : 0.06231403350830078\n",
      "           astar_tot : 0.05080842971801758\n",
      "               total : 0.16878604888916016\n",
      "     points_to_visit : 4161\n",
      "     obstacle_points : 3039\n",
      "      points_visited : 4161\n",
      "   coverage_path_len : 4597\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.10478250420571977\n",
      "          area_shape : (50, 144)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 13 ::  (50, 144) comb_6.png\n",
      "            bous_avg : 0.004716277122497559\n",
      "          btrack_avg : 0.0033800899982452393\n",
      "           astar_avg : 0.008722909291585286\n",
      "            bous_tot : 0.07546043395996094\n",
      "          btrack_tot : 0.05408143997192383\n",
      "           astar_tot : 0.1308436393737793\n",
      "               total : 0.2605278491973877\n",
      "     points_to_visit : 6042\n",
      "     obstacle_points : 1158\n",
      "      points_visited : 6042\n",
      "   coverage_path_len : 6823\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.12926183382985768\n",
      "          area_shape : (50, 144)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 14 ::  (144, 256) comb_7.png\n",
      "            bous_avg : 0.0014935346041324634\n",
      "          btrack_avg : 0.011046377357077483\n",
      "           astar_avg : 0.004979571092475966\n",
      "            bous_tot : 0.3091616630554199\n",
      "          btrack_tot : 2.286600112915039\n",
      "           astar_tot : 1.0257916450500488\n",
      "               total : 3.6228737831115723\n",
      "     points_to_visit : 25866\n",
      "     obstacle_points : 10998\n",
      "      points_visited : 25866\n",
      "   coverage_path_len : 28060\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.08482177375705557\n",
      "          area_shape : (144, 256)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 15 ::  (50, 144) comb_3.png\n",
      "            bous_avg : 0.0005339840862238519\n",
      "          btrack_avg : 0.002218738894596278\n",
      "           astar_avg : 0.000689565010790555\n",
      "            bous_tot : 0.05713629722595215\n",
      "          btrack_tot : 0.23740506172180176\n",
      "           astar_tot : 0.07309389114379883\n",
      "               total : 0.3682999610900879\n",
      "     points_to_visit : 3680\n",
      "     obstacle_points : 3520\n",
      "      points_visited : 3680\n",
      "   coverage_path_len : 4602\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.25054347826086953\n",
      "          area_shape : (50, 144)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 16 ::  (32, 32) comb_11.png\n",
      "            bous_avg : 0.00030965696681629527\n",
      "          btrack_avg : 0.0010551918636668813\n",
      "           astar_avg : 0.0005224305529927098\n",
      "            bous_tot : 0.013624906539916992\n",
      "          btrack_tot : 0.04642844200134277\n",
      "           astar_tot : 0.022464513778686523\n",
      "               total : 0.0827491283416748\n",
      "     points_to_visit : 772\n",
      "     obstacle_points : 252\n",
      "      points_visited : 772\n",
      "   coverage_path_len : 1114\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.4430051813471503\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 17 ::  (50, 50) comb_2.png\n",
      "            bous_avg : 0.00043979956179249045\n",
      "          btrack_avg : 0.0013287845922976124\n",
      "           astar_avg : 0.0007332464059193929\n",
      "            bous_tot : 0.02155017852783203\n",
      "          btrack_tot : 0.06511044502258301\n",
      "           astar_tot : 0.03519582748413086\n",
      "               total : 0.12210702896118164\n",
      "     points_to_visit : 1318\n",
      "     obstacle_points : 1182\n",
      "      points_visited : 1318\n",
      "   coverage_path_len : 1800\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.3657056145675266\n",
      "          area_shape : (50, 50)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 18 ::  (50, 50) comb_0.png\n",
      "            bous_avg : 0.0007387399673461914\n",
      "          btrack_avg : 0.0012377926281520299\n",
      "           astar_avg : 0.0003067740687617549\n",
      "            bous_tot : 0.02068471908569336\n",
      "          btrack_tot : 0.034658193588256836\n",
      "           astar_tot : 0.008282899856567383\n",
      "               total : 0.06380581855773926\n",
      "     points_to_visit : 1529\n",
      "     obstacle_points : 971\n",
      "      points_visited : 1529\n",
      "   coverage_path_len : 1700\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.11183780248528441\n",
      "          area_shape : (50, 50)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 19 ::  (32, 32) comb_12.png\n",
      "            bous_avg : 0.0001989910679478799\n",
      "          btrack_avg : 0.0009345623754685925\n",
      "           astar_avg : 0.00037555225559922513\n",
      "            bous_tot : 0.012337446212768555\n",
      "          btrack_tot : 0.057942867279052734\n",
      "           astar_tot : 0.022908687591552734\n",
      "               total : 0.09347295761108398\n",
      "     points_to_visit : 610\n",
      "     obstacle_points : 414\n",
      "      points_visited : 610\n",
      "   coverage_path_len : 1244\n",
      "            coverage : 1.0\n",
      "          redundancy : 1.039344262295082\n",
      "          area_shape : (32, 32)\n",
      "adjacency test pass : True\n",
      "\n",
      "# 20 ::  (50, 50) comb_1.png\n",
      "            bous_avg : 0.0006266902474796071\n",
      "          btrack_avg : 0.0012745156007654527\n",
      "           astar_avg : 0.0009198839014226741\n",
      "            bous_tot : 0.02130746841430664\n",
      "          btrack_tot : 0.04333353042602539\n",
      "           astar_tot : 0.030356168746948242\n",
      "               total : 0.09517312049865723\n",
      "     points_to_visit : 1472\n",
      "     obstacle_points : 1028\n",
      "      points_visited : 1472\n",
      "   coverage_path_len : 1881\n",
      "            coverage : 1.0\n",
      "          redundancy : 0.2778532608695652\n",
      "          area_shape : (50, 50)\n",
      "adjacency test pass : True\n",
      "\n",
      "\n"
     ]
    }
   ],
   "source": [
    "display = False\n",
    "for i,path in tqdm(enumerate(Path(\"./test_maps/\").iterdir()),total=21):\n",
    "    if path.suffix != \".png\":\n",
    "        continue\n",
    "    area_map = get_area_map(path)\n",
    "    # Remove conditional to run on all maps.\n",
    "#     if area_map.shape[1] > 50:\n",
    "#         continue\n",
    "    print(f\"# {i} :: \",area_map.shape, path.name)\n",
    "    start_point = get_random_coords(area_map, 1)[0]\n",
    "    matrix = bous_preprocess(area_map)\n",
    "    coverage_path = do_everything(matrix, start_point)\n",
    "    end_point = coverage_path[-1]\n",
    "    printer(coverage_metrics(area_map, coverage_path))\n",
    "    print(f\"adjacency test pass : {adjacency_test(coverage_path)}\")\n",
    "    print()\n",
    "\n",
    "    if display:\n",
    "        imshow(matrix, cmap=\"Greys_r\",figsize=(10,10))\n",
    "        imshow_scatter(coverage_path, alpha=0.5, color=\"lightblue\",s=4)\n",
    "        imshow_scatter([start_point], color=\"black\")\n",
    "        imshow_scatter([end_point],color=\"red\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
